// very loosely based on https://github.com/ianstormtaylor/slate/blob/d22c76ae1313fe82111317417912a2670e73f5c9/site/examples/paste-html.tsx
import { Node } from "slate"
import { isBlock } from ".."
import {
  addMarksToChildren,
  getInlineNodes,
  forceDisableMarkForChildren,
  setLinkForChildren
} from "./utils"

function getAlignmentFromElement(element) {
  const parent = element.parentElement
  // confluence
  const attribute = parent?.getAttribute("data-align")
  // note: we don't show html that confluence would parse as alignment
  // we could change that but meh
  // (they match on div.fabric-editor-block-mark with data-align)
  if (attribute === "center" || attribute === "end") {
    return attribute
  }
  if (element instanceof HTMLElement) {
    // Google docs
    const textAlign = element.style.textAlign
    if (textAlign === "center") {
      return "center"
    }
    // TODO: RTL things?
    if (textAlign === "right" || textAlign === "end") {
      return "end"
    }
  }
}

const headings = {
  H1: 1,
  H2: 2,
  H3: 3,
  H4: 4,
  H5: 5,
  H6: 6
}

const TEXT_TAGS = {
  CODE: "code",
  DEL: "strikethrough",
  S: "strikethrough",
  STRIKE: "strikethrough",
  EM: "italic",
  I: "italic",
  STRONG: "bold",
  U: "underline",
  SUP: "superscript",
  SUB: "subscript",
  KBD: "keyboard"
}

function marksFromElementAttributes(element) {
  const marks = new Set()
  const style = element.style
  const { nodeName } = element
  const markFromNodeName = TEXT_TAGS[nodeName]
  if (markFromNodeName) {
    marks.add(markFromNodeName)
  }
  const { fontWeight, textDecoration, verticalAlign } = style

  if (textDecoration === "underline") {
    marks.add("underline")
  } else if (textDecoration === "line-through") {
    marks.add("strikethrough")
  }
  // confluence
  if (nodeName === "SPAN" && element.classList.contains("code")) {
    marks.add("code")
  }
  // Google Docs does weird things with <b>
  if (nodeName === "B" && fontWeight !== "normal") {
    marks.add("bold")
  } else if (
    typeof fontWeight === "string" &&
    (fontWeight === "bold" ||
      fontWeight === "bolder" ||
      fontWeight === "1000" ||
      /^[5-9]\d{2}$/.test(fontWeight))
  ) {
    marks.add("bold")
  }
  if (style.fontStyle === "italic") {
    marks.add("italic")
  }
  // Google Docs uses vertical align for subscript and superscript instead of <sup> and <sub>
  if (verticalAlign === "super") {
    marks.add("superscript")
  } else if (verticalAlign === "sub") {
    marks.add("subscript")
  }
  return marks
}

export function deserializeHTML(html) {
  const parsed = new DOMParser().parseFromString(html, "text/html")
  return fixNodesForBlockChildren(deserializeNodes(parsed.body.childNodes))
}

export function deserializeHTMLNode(el) {
  if (!(el instanceof globalThis.HTMLElement)) {
    const text = el.textContent
    if (!text) {
      return []
    }
    return getInlineNodes(text)
  }
  if (el.nodeName === "BR") {
    return getInlineNodes("\n")
  }

  if (el.nodeName === "IMG") {
    const alt = el.getAttribute("alt")
    return getInlineNodes(alt ?? "")
  }

  if (el.nodeName === "HR") {
    return [{ type: "divider", children: [{ text: "" }] }]
  }

  const marks = marksFromElementAttributes(el)

  // Dropbox Paper displays blockquotes as lists for some reason
  if (el.classList.contains("listtype-quote")) {
    marks.delete("italic")
    return addMarksToChildren(marks, () => [
      {
        type: "blockquote",
        children: fixNodesForBlockChildren(deserializeNodes(el.childNodes))
      }
    ])
  }

  return addMarksToChildren(marks, () => {
    const { nodeName } = el

    if (nodeName === "A") {
      const href = el.getAttribute("href")
      if (href) {
        return setLinkForChildren(href, () =>
          forceDisableMarkForChildren("underline", () =>
            deserializeNodes(el.childNodes)
          )
        )
      }
    }

    if (nodeName === "PRE" && el.textContent) {
      return [{ type: "code", children: [{ text: el.textContent || "" }] }]
    }

    const deserialized = deserializeNodes(el.childNodes)
    const children = fixNodesForBlockChildren(deserialized)

    if (nodeName === "LI") {
      let nestedList

      const listItemContent = {
        type: "list-item-content",
        children: children.filter(node => {
          if (
            nestedList === undefined &&
            (node.type === "ordered-list" || node.type === "unordered-list")
          ) {
            nestedList = node
            return false
          }
          return true
        })
      }
      const listItemChildren = nestedList
        ? [listItemContent, nestedList]
        : [listItemContent]
      return [{ type: "list-item", children: listItemChildren }]
    }

    if (nodeName === "P") {
      return [
        { type: "paragraph", textAlign: getAlignmentFromElement(el), children }
      ]
    }

    const headingLevel = headings[nodeName]

    if (typeof headingLevel === "number") {
      return [
        {
          type: "heading",
          level: headingLevel,
          textAlign: getAlignmentFromElement(el),
          children
        }
      ]
    }

    if (nodeName === "BLOCKQUOTE") {
      return [{ type: "blockquote", children }]
    }
    if (nodeName === "OL") {
      return [{ type: "ordered-list", children }]
    }
    if (nodeName === "UL") {
      return [{ type: "unordered-list", children }]
    }
    if (nodeName === "DIV" && !isBlock(children[0])) {
      return [{ type: "paragraph", children }]
    }
    return deserialized
  })
}

function deserializeNodes(nodes) {
  const outputNodes = []
  for (const node of nodes) {
    outputNodes.push(...deserializeHTMLNode(node))
  }
  return outputNodes
}

function fixNodesForBlockChildren(deserializedNodes) {
  if (!deserializedNodes.length) {
    // Slate also gets unhappy if an element has no children
    // the empty text nodes will get normalized away if they're not needed
    return [{ text: "" }]
  }
  if (deserializedNodes.some(isBlock)) {
    const result = []
    let queuedInlines = []
    const flushInlines = () => {
      if (queuedInlines.length) {
        result.push({ type: "paragraph", children: queuedInlines })
        queuedInlines = []
      }
    }
    for (const node of deserializedNodes) {
      if (isBlock(node)) {
        flushInlines()
        result.push(node)
        continue
      }
      // we want to ignore whitespace between block level elements
      // useful info about whitespace in html:
      // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
      if (Node.string(node).trim() !== "") {
        queuedInlines.push(node)
      }
    }
    flushInlines()
    return result
  }
  return deserializedNodes
}
